package com.github.passerr.idea.plugins.tool.log
/*
 * Original class is here:
 * https://github.com/hibernate/hibernate-orm/blob/a30635f14ae272fd63a653f9a9e1a9aeb390fad4/hibernate-core/src/main
 * /java/org/hibernate/engine/jdbc/internal/BasicFormatterImpl.java
 *
 * it imported org.hibernate.internal.util.StringHelper, but it doesn't really need that.
 */

//import org.hibernate.internal.util.StringHelper;

/**
 * Performs formatting of basic SQL statements (DML + query).
 *
 * @author Gavin King
 * @author Steve Ebersole
 */
class SqlFormatter {

    // MOD: from org.hibernate.internal.util.StringHelper
    private static final String WHITESPACE = " \n\r\f\t"

    private static final Set<String> BEGIN_CLAUSES = new HashSet<String>()
    private static final Set<String> END_CLAUSES = new HashSet<String>()
    private static final Set<String> LOGICAL = new HashSet<String>()
    private static final Set<String> QUANTIFIERS = new HashSet<String>()
    private static final Set<String> DML = new HashSet<String>()
    private static final Set<String> MISC = new HashSet<String>()

    static {
        BEGIN_CLAUSES.add("left")
        BEGIN_CLAUSES.add("right")
        BEGIN_CLAUSES.add("inner")
        BEGIN_CLAUSES.add("outer")
        BEGIN_CLAUSES.add("group")
        BEGIN_CLAUSES.add("order")

        END_CLAUSES.add("where")
        END_CLAUSES.add("set")
        END_CLAUSES.add("having")
        END_CLAUSES.add("join")
        END_CLAUSES.add("from")
        END_CLAUSES.add("by")
        END_CLAUSES.add("join")
        END_CLAUSES.add("into")
        END_CLAUSES.add("union")

        LOGICAL.add("and")
        LOGICAL.add("or")
        LOGICAL.add("when")
        LOGICAL.add("else")
        LOGICAL.add("end")

        QUANTIFIERS.add("in")
        QUANTIFIERS.add("all")
        QUANTIFIERS.add("exists")
        QUANTIFIERS.add("some")
        QUANTIFIERS.add("any")

        DML.add("insert")
        DML.add("update")
        DML.add("delete")

        MISC.add("select")
        MISC.add("on")
    }

    private static final String INDENT_STRING = "  "
    // MOD: Make initial indent zero
    private static final String INITIAL = /*System.lineSeparator() + INDENT_STRING*/ ""

    static String format(String source) {
        return new FormatProcess(source).perform()
    }

    private static class FormatProcess {
        boolean beginLine = true
        boolean afterBeginBeforeEnd
        boolean afterByOrSetOrFromOrSelect
        boolean afterValues
        boolean afterOn
        boolean afterBetween
        boolean afterInsert
        int inFunction
        int parensSinceSelect
        private LinkedList<Integer> parenCounts = new LinkedList<Integer>()
        private LinkedList<Boolean> afterByOrFromOrSelects = new LinkedList<Boolean>()

        // MOD: Make BOL indent zero
        int indent = /*1*/ 0

        StringBuilder result = new StringBuilder()
        StringTokenizer tokens
        String lastToken
        String token
        String lcToken

        FormatProcess(String sql) {
            tokens = new StringTokenizer(
                sql,
                "()+*/-=<>'`\"[]," + /*StringHelper.*/WHITESPACE,
                true
            )
        }

        String perform() {

            result.append(INITIAL)

            while (tokens.hasMoreTokens()) {
                token = tokens.nextToken()
                lcToken = token.toLowerCase(Locale.ROOT)

                if ("'" == token) {
                    String t
                    // cannot handle single quotes
                    while ("'" != t && tokens.hasMoreTokens()){
                        t = tokens.nextToken()
                        token += t
                    }
                } else if ("\"" == token) {
                    String t
                    while ("\"" != t && tokens.hasMoreTokens()){
                        t = tokens.nextToken()
                        token += t
                    }
                }
                // SQL Server uses "[" and "]" to escape reserved words
                // see SQLServerDialect.openQuote and SQLServerDialect.closeQuote
                else if ("[" == token) {
                    String t
                    while ("]" != t && tokens.hasMoreTokens()){
                        t = tokens.nextToken()
                        token += t
                    }
                }

                if (afterByOrSetOrFromOrSelect && "," == token) {
                    commaAfterByOrFromOrSelect()
                } else if (afterOn && "," == token) {
                    commaAfterOn()
                } else if ("(" == token) {
                    openParen()
                } else if (")" == token) {
                    closeParen()
                } else if (BEGIN_CLAUSES.contains(lcToken)) {
                    beginNewClause()
                } else if (END_CLAUSES.contains(lcToken)) {
                    endNewClause()
                } else if ("select" == lcToken) {
                    select()
                } else if (DML.contains(lcToken)) {
                    updateOrInsertOrDelete()
                } else if ("values" == lcToken) {
                    values()
                } else if ("on" == lcToken) {
                    on()
                } else if (afterBetween && lcToken == "and") {
                    misc()
                    afterBetween = false
                } else if (LOGICAL.contains(lcToken)) {
                    logical()
                } else if (isWhitespace(token)) {
                    white()
                } else {
                    misc()
                }

                if (!isWhitespace(token)) {
                    lastToken = lcToken
                }

            }
            return result.toString()
        }

        private void commaAfterOn() {
            out()
            indent--
            newline()
            afterOn = false
            afterByOrSetOrFromOrSelect = true
        }

        private void commaAfterByOrFromOrSelect() {
            out()
            newline()
        }

        private void logical() {
            if ("end" == lcToken) {
                indent--
            }
            newline()
            out()
            beginLine = false
        }

        private void on() {
            indent++
            afterOn = true
            newline()
            out()
            beginLine = false
        }

        private void misc() {
            out()
            if ("between" == lcToken) {
                afterBetween = true
            }
            if (afterInsert) {
                newline()
                afterInsert = false
            } else {
                beginLine = false
                if ("case" == lcToken) {
                    indent++
                }
            }
        }

        private void white() {
            if (!beginLine) {
                result.append(" ")
            }
        }

        private void updateOrInsertOrDelete() {
            out()
            indent++
            beginLine = false
            if ("update" == lcToken) {
                newline()
            }
            if ("insert" == lcToken) {
                afterInsert = true
            }
        }

        private void select() {
            out()
            indent++
            newline()
            parenCounts.addLast(parensSinceSelect)
            afterByOrFromOrSelects.addLast(afterByOrSetOrFromOrSelect)
            parensSinceSelect = 0
            afterByOrSetOrFromOrSelect = true
        }

        private void out() {
            result.append(token)
        }

        private void endNewClause() {
            if (!afterBeginBeforeEnd) {
                indent--
                if (afterOn) {
                    indent--
                    afterOn = false
                }
                newline()
            }
            out()
            if ("union" != lcToken) {
                indent++
            }
            newline()
            afterBeginBeforeEnd = false
            afterByOrSetOrFromOrSelect = "by" == lcToken || "set" == lcToken || "from" == lcToken
        }

        private void beginNewClause() {
            if (!afterBeginBeforeEnd) {
                if (afterOn) {
                    indent--
                    afterOn = false
                }
                indent--
                newline()
            }
            out()
            beginLine = false
            afterBeginBeforeEnd = true
        }

        private void values() {
            indent--
            newline()
            out()
            indent++
            newline()
            afterValues = true
        }

        private void closeParen() {
            parensSinceSelect--
            if (parensSinceSelect < 0) {
                indent--
                parensSinceSelect = parenCounts.removeLast()
                afterByOrSetOrFromOrSelect = afterByOrFromOrSelects.removeLast()
            }
            if (inFunction > 0) {
                inFunction--
                out()
            } else {
                if (!afterByOrSetOrFromOrSelect) {
                    indent--
                    newline()
                }
                out()
            }
            beginLine = false
        }

        private void openParen() {
            if (isFunctionName(lastToken) || inFunction > 0) {
                inFunction++
            }
            beginLine = false
            if (inFunction > 0) {
                out()
            } else {
                out()
                if (!afterByOrSetOrFromOrSelect) {
                    indent++
                    newline()
                    beginLine = true
                }
            }
            parensSinceSelect++
        }

        private static boolean isFunctionName(String tok) {
            if (tok == null || tok.length() == 0) {
                return false
            }

            final char begin = tok.charAt(0)
            final boolean isIdentifier = Character.isJavaIdentifierStart(begin) || '"' as char == begin
            return isIdentifier &&
                   !LOGICAL.contains(tok) &&
                   !END_CLAUSES.contains(tok) &&
                   !QUANTIFIERS.contains(tok) &&
                   !DML.contains(tok) &&
                   !MISC.contains(tok)
        }

        private static boolean isWhitespace(String token) {
            return /*StringHelper.*/ WHITESPACE.contains(token)
        }

        private void newline() {
            result.append(System.lineSeparator())
            for (int i = 0; i < indent; i++) {
                result.append(INDENT_STRING)
            }
            beginLine = true
        }
    }
}